/*
 * Copyright (c) United Binary LLC.  All rights reserved.
 * 
 * This code is licensed under the MIT License
 * 
 * SEE: http://harnessit.codeplex.com/license
 * 
 */
using System;
using System.Reflection;
using System.Text;
using System.Threading;
using UnitedBinary.UnitTests.Framework;
using UnitedBinary.UnitTests.Framework.TestItems;

namespace UnitedBinary.HarnessIt.SharedTypes
{
	/// <summary>
	///		The Method class represents a method, with emphasis on unit test methods.
	/// </summary>
	[Serializable]
	public class Method : IComparable, IMethod
	{
		#region Member Variables

		private MethodInfo myInfo;
		private bool active;
		private TestMethodRecord tmr = new TestMethodRecord();
		private Class myClass;
		private double runningTime = 0.0;
		private string name;
		private string className;
		private string desc = "";
		private bool testRequired;
		private bool isOrdered;
		private int subOrderMode = 100;
		private TestOrderModes orderMode = TestOrderModes.Neutral;

		#endregion

		#region Construction and Intialization

		/// <summary>
		///		Initializing constructor.
		/// </summary>
		/// <param name="methodInfo">MethodInfo reflection information about this method.</param>
		/// <param name="containingClass">A reference to the parent Class.</param>
		public Method(MethodInfo methodInfo, Class containingClass, TestMethodAttribute tma)
		{
			if (methodInfo == null)
			{
				throw new ArgumentNullException("methodInfo", "Method info cannot be null.");
			}
			else if (containingClass == null)
			{
				throw new ArgumentNullException("containingClass", "Containing class cannot be null.");
			}
			else if (tma == null)
			{
				throw new ArgumentNullException("tma", "TestMethod attribute cannot be null.");
			}

			myInfo = methodInfo;
			myClass = containingClass;
			name = myInfo.Name;
			className = myInfo.DeclaringType.Name;
			desc = tma.Description;
			this.Active = true; // increments count in parent class, too.
			testRequired = Attribute.GetCustomAttribute(myInfo, typeof(TestRequiredAttribute), true) != null;
			if (testRequired)
			{
				myClass.RequiredTestCount++;
			}

			TestOrderAttribute toa = (TestOrderAttribute)Attribute.GetCustomAttribute(
				myInfo, typeof(TestOrderAttribute), true);
			isOrdered = toa != null;
			if (isOrdered)
			{
				orderMode = toa.Mode;
				subOrderMode = toa.SubModeOrder;
			}
		}

		#endregion

		#region Unit Test Related Code

		/// <summary>
		///		RunTest executes the unit test method and generates a new TestMethodRecord.
		/// </summary>
		public void RunTest(object target)
		{
			tmr = new TestMethodRecord();
			TestMethodRecord.Current = tmr;
			tmr.HasRun = true;

			AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
			try
			{
				object[] args;
				if (MethodAcceptsTestMethodRecord())
				{
					args = new object[] { tmr };
				}
				else
				{
					args = new object[0];
				}

				try
				{
					myInfo.DeclaringType.InvokeMember(
						myInfo.Name, BindingFlags.Default | BindingFlags.InvokeMethod,
						null, target, args);
				}
				finally
				{
					FrameworkExtensions.RunAndClearAllOpenTests();
				}
			}
			catch (FailedTestException z)
			{
				Console.WriteLine("I'M HERE: " + z);
			}
			catch (Exception e)
			{
				if (!(e.InnerException != null && (e.InnerException is FailedTestException)))
				{
					ProcessException(e);					
				}
			}
			finally
			{
				AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;

				if (tmr.ExpectException && tmr.TestExceptionCount == 0)
				{
					StringBuilder sb = new StringBuilder();

					sb.Append("Expected Exception NOT THROWN: ");
					sb.Append(tmr.ExpectedExceptionMessage);

					tmr.RunTest(false, sb.ToString());
				}

				TestMethodRecord.Current = null;
			}
		}

		private bool MethodAcceptsTestMethodRecord()
		{
			ValidateSignature();

			ParameterInfo[] pi = myInfo.GetParameters();
			return pi.Length == 1;
		}

		private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			ProcessException((Exception)e.ExceptionObject);
		}

		private void ProcessException(Exception e)
		{
			TestException te = new TestException(e);
			te.ThreadID = Thread.CurrentThread.ManagedThreadId;
			te.ThreadName = Thread.CurrentThread.Name;

			tmr.AddTestException(te);

			Exception E = (e is TargetInvocationException) ? e.InnerException : e;

			string assemblyName = this.ParentClass.ParentAssembly.Name;
			string parentClassName = this.ParentClass.Name;

			StringBuilder sb = new StringBuilder();
			sb.Append("http://ExceptionExplorer/");
			sb.Append(assemblyName);
			sb.Append("/");
			sb.Append(parentClassName);
			sb.Append("/");
			sb.Append(this.Name);
			sb.Append("?ID=");
			sb.Append(tmr.TestExceptionCount.ToString());

			string exceptionURL = sb.ToString().Replace(' ', '+');

			sb = new StringBuilder();

			if (tmr.TestExceptionCount == 1)
			{
				if (tmr.ExpectException)
				{
					if (tmr.ConsistentException)
					{
						sb.Append("Expected exception caught: ");
						sb.Append(tmr.ExpectedExceptionMessage);
						sb.Append("\n");
						sb.Append("View exception details: ");
						sb.Append(exceptionURL);

						AppendThreadInformation(sb, te);

						tmr.RunTest(true, sb.ToString());
					}
					else
					{
						sb.Append("INCONSISTENT expected exception:\nExpected type ");
						sb.Append(tmr.ExpectedExceptionType);
						sb.Append(" but caught exception of type ");
						sb.Append(E.GetType());
						sb.Append(".\n");
						sb.Append("Test message: ");
						sb.Append(tmr.ExpectedExceptionMessage);
						sb.Append("\n");
						sb.Append("Exception message: ");
						sb.Append(GetDetailedExceptionMessage(E));
						sb.Append("\n");
						sb.Append("View exception details: ");
						sb.Append(exceptionURL);

						AppendThreadInformation(sb, te);

						tmr.RunTest(false, sb.ToString());
					}
				}
				else
				{
					string detailedErrorMessage = GetDetailedExceptionMessage(E);

					sb.Append("UNHANDLED EXCEPTION CAUGHT: ");
					sb.Append(detailedErrorMessage);
					sb.Append("\n");
					sb.Append("View exception details: ");
					sb.Append(exceptionURL);

					AppendThreadInformation(sb, te);

					string methodText = "Void UnitedBinary.UnitTests.Framework.TestMethodRecord.RunTest(Boolean, System.String)";
					if (detailedErrorMessage.IndexOf(methodText) >= 0)
					{
						sb.Append("\nHarnessIt warning: This error is most likely due an out-of-date ");
						sb.Append("test basis DLL. Please ensure that you have compiled this unit test ");
						sb.Append("assembly while referencing the latest test basis DLL.");
					}

					tmr.RunTest(false, sb.ToString());
				}
			}
			else
			{
				sb.Append("Thread exception caught: ");
				sb.Append(tmr.ExpectedExceptionMessage);
				sb.Append("\n");
				sb.Append("View exception details: ");
				sb.Append(exceptionURL);

				AppendThreadInformation(sb, te);

				tmr.RunTest(true, sb.ToString());
			}
		}

		private void AppendThreadInformation(StringBuilder sb, TestException te)
		{
			sb.Append("\n");
			sb.Append("Thread information: ID=0x");
			sb.Append(te.ThreadID.ToString("X6"));
			if (te.ThreadName.Length > 0)
			{
				sb.Append(", Name=");
				sb.Append(te.ThreadName);
			}
		}

		private void ValidateSignature()
		{
			ParameterInfo[] pi = myInfo.GetParameters();

			if (pi.Length == 0)
			{
				return;
			}

			if (pi.Length > 1)
			{
				throw new Exception("Method does not match the required signature: public void " +
									 "Method(TestMethodRecord tmr), too many arguments.");
			}

			//
			// We used to test for the name matching directly, but the obfuscated
			// version seemed to append an & to the end of the name in pi!
			//
			string paramName = pi[0].ParameterType.FullName;
			if (paramName.IndexOf("TestMethodRecord") < 0 && paramName.IndexOf("ITest") < 0)
			{
				throw new Exception("Method does not match the required signature: public void " +
									 "Method(ITest it) or public void Method(TestMethodRecord tmr), " +
									 "invalid arguement type.");
			}
		}

		private string GetDetailedExceptionMessage(Exception e)
		{
			StringBuilder sb = new StringBuilder();

			Exception exp = e;
			while (exp != null)
			{
				sb.Append("[");
				sb.Append(exp.GetType());
				sb.Append("] ");
				sb.Append(exp.Message);

				if (exp.InnerException != null)
				{
					sb.Append(" --> ");
				}
				exp = exp.InnerException;
			}

			return sb.ToString();
		}

		/// <summary>
		///		TestRecord is the TestMethodRecord of this method.
		/// </summary>
		public TestMethodRecord TestRecord
		{
			get { return tmr; }
			set { tmr = value; }
		}

		/// <summary>
		///		HasRun indicates whether the method has been run.
		/// </summary>
		public bool HasRun
		{
			get { return TestRecord.HasRun; }
		}

		/// <summary>
		///		Name is the name of this method.
		/// </summary>
		public string Name
		{
			get { return name; }
		}

		/// <summary>
		///		ClassName is the name of the parent class.
		/// </summary>
		public string ClassName
		{
			get { return className; }
		}

		public Class ParentClass
		{
			get { return myClass; }
		}

		/// <summary>
		///		Description is the description associated with the TestMethod custom attribute.
		/// </summary>
		public string Description
		{
			get { return desc; }
		}

		public TestOrderModes OrderMode
		{
			get { return orderMode; }
		}

		public int SubModeOrder
		{
			get { return subOrderMode; }
		}

		public bool IsOrdered
		{
			get { return isOrdered; }
		}

		public bool TestRequired
		{
			get { return testRequired; }
		}

		/// <summary>
		///		Indicates whehter this method is active and should be run.
		/// </summary>
		public bool Active
		{
			get { return active; }
			set { active = value; }
		}

		/// <summary>
		///		Resets the TestMethodRecord data associated with this method.
		/// </summary>
		public void ResetTest()
		{
			tmr = new TestMethodRecord();
			runningTime = 0.0;
		}

		/// <summary>
		///		RunningTime for this method in seconds.
		/// </summary>
		public double RunningTime
		{
			get { return runningTime; }
			set { runningTime = Math.Max(0.0, value); }
		}

		#endregion

		#region IComparable Implementation

		public int CompareTo(object obj)
		{
			Method m = obj as Method;

			if (m == null)
			{
				throw new ArgumentException("Could not compare objects. Invalid object type or object null.");
			}

			// If they have the same order mode, we must compare the 
			// submode order to decide which should come first.
			if (this.OrderMode == m.OrderMode)
			{
				if (this.SubModeOrder == m.SubModeOrder)
				{
					return 0;
				}
				else if (this.SubModeOrder < m.SubModeOrder)
				{
					return -1;
				}
				return 1;
			}

			// If we are here, ordermodes are not equal and
			// this class's order mode is first (so m's isn't first).
			// This class should come first.
			if (this.OrderMode == TestOrderModes.First)
			{
				return -1;
			}

			// If we are here, ordermodes are not equal and
			// this class's order mode is last (so m's isn't last).
			// m should come first.
			if (this.OrderMode == TestOrderModes.Last)
			{
				return 1;
			}

			// This class's order mode isn't first or last
			// so we must determine whether m's order mode
			// is first or last (those are the only possiblities
			// left.
			if (m.OrderMode == TestOrderModes.First)
			{
				return 1;
			}

			if (m.OrderMode == TestOrderModes.Last)
			{
				return -1;
			}

			return 0;
		}

		#endregion

		#region IMethod Members


		IClass IMethod.ParentClass
		{
			get { return ParentClass; }
		}

		#endregion
	}
}